import * as THREE from "three";

// 引入轨道控制器控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// 引入动画库
import gsap from "gsap";
// 引入数据图形用户界面库
import * as dat from "dat.gui";
// 引入rgbeLoad
// DataTextureLoader
// 用于加载二进制文件格式的(rgbe, hdr, ...)的抽象类。 内部使用FileLoader来加载文件， 和创建一个新的 DataTexture.
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"
// 引入物理引擎
import * as CANNON from 'cannon-es';


console.log(CANNON);

//three.js 基本内容

// 1.创建场景  场景能够让你在什么地方、摆放什么东西来交给three.js来渲染，这是你放置物体、灯光和摄像机的地方。
const scene = new THREE.Scene();

// 2.创建相机 

//  透视相机（PerspectiveCamera）这一摄像机使用perspective projection（透视投影）来进行投影。
// 这一投影模式被用来模拟人眼所看到的景象，它是3D场景的渲染中使用得最普遍的投影模式。
// PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
// fov — 摄像机视锥体垂直视野角度   // aspect — 摄像机视锥体长宽比  // near — 摄像机视锥体近端面  // far — 摄像机视锥体远端面
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);

//2.1设置相机的位置 position表示对象局部位置的Vector3。默认值为(0, 0, 0)。

// Vector3( x : Float, y : Float, z : Float )   该类表示的是一个三维向量（3D vector）。'
//  一个三维向量表示的是一个有顺序的、三个为一组的数字组合（标记为x、y和z）， 可被用来表示很多事物:
//  一个位于三维空间中的点。
// 一个在三维空间中的方向与长度的定义。在three.js中，长度总是从(0, 0, 0)到(x, y, z)的 Euclidean distance（欧几里德距离，即直线距离）， 
// 方向也是从(0, 0, 0)到(x, y, z)的方向。
// .set ( x : Float, y : Float, z : Float ) : this
// 设置该向量的x、y 和 z 分量。

camera.position.set(0, 0, 20);
scene.add(camera); //添加相机到场景中

// 创建球和平面
const boxMaterial = new THREE.MeshStandardMaterial();    // 创建物理材质
const boxWorldMaterial = new CANNON.Material("box");
document.body.click();
const hitSound = new Audio("mp3/hitSound1.mp3");
function HitEvent(e) {
    //获取碰撞的强度
    const impactStrenght = e.contact.getImpactVelocityAlongNormal();
    // console.log(impactStrenght);
    // 如果强度大于4播放声音
    if (impactStrenght > 1) {
        hitSound.currentTime = 0;
        // 根据强度调整播放声音
        hitSound.volume =  impactStrenght / 10 > 1 ? 1 :  impactStrenght / 10; // 0 ~ 1
        hitSound.play()
    }
}
let boxArr = [];
function createBox() {
    const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
    const box = new THREE.Mesh(boxGeometry, boxMaterial);
    box.castShadow = true;
    scene.add(box);

    // 创建物理小球形状
    const boxShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));

    // 创建物理世界的物体
    const boxBody = new CANNON.Body({
        // 添加的形状
        shape: boxShape,
        // 物体的质量
        mass: 1,
        // 物体的物理材料。它定义了身体与其他身体的相互作用。
        material: boxWorldMaterial,
        // 物体的位置
        position: new CANNON.Vec3(0, 0, 0)
    });
    // 给物体施加力 对身体的某个局部施加力。
    boxBody.applyLocalForce(
        new CANNON.Vec3(500,0,800),//添加的力的大小和方向
        new CANNON.Vec3(0,0,0)//施加力的位置(点)
    );

    // 将物体添加到物理世界模拟
    world.addBody(boxBody);

    // 监听碰撞然后播放音效
    boxBody.addEventListener("collide", HitEvent);
    boxArr.push({
        mesh: box,
        body: boxBody
    });
}
window.addEventListener("click",createBox)

const floor = new THREE.Mesh(
    new THREE.PlaneBufferGeometry(20, 20),
    new THREE.MeshStandardMaterial()
);

floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);

// 使用物理引擎创建物理世界
// const world = new CANNON.World({gravity : 9.8});
const world = new CANNON.World();
// 设置世界的重力。
world.gravity.set(0, -9.82, 0);

// 创建物理世界的地面
const floorShape = new CANNON.Plane();
const floorWorldMaterial = new CANNON.Material("floor");
const floorBody = new CANNON.Body();
// 当质量为0时候，可以使物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0)
// 平面旋转  Quaternion 世界空间定位
// 四元数描述三维空间中的旋转。四元数的数学定义为 Q = xi + yj + z*k + w，
// 其中 (i,j,k) 是虚基向量。(x,y,z)可视为与旋转轴相关的向量，而实数乘数 w 则与旋转量相关。
// setFromAxisAngle 给定一个轴和一个角度（以弧度为单位）
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
floorBody.material = floorWorldMaterial;

// 将物体添加到物理世界模拟
world.addBody(floorBody);

// 设置两种材质的碰撞的参数 
// 定义两种材料相遇时发生的情况。
const contactMaterial = new CANNON.ContactMaterial(boxWorldMaterial, floorWorldMaterial, {
    // 摩擦力 摩擦系数。
    friction: 0.1,
    // 弹性 复原系数。
    restitution: 0.7
});
// 为物理世界添加接触材料
world.addContactMaterial(contactMaterial);
// 设置世界碰撞的默认材料,如果没有设置接触材料设置,都用这个
world.defaultContactMaterial = contactMaterial;

// 添加灯光
// 添加环境光
const light = new THREE.AmbientLight(0xffffff, 2);

// 添加平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 3);
// // 设置平行光光源位置
directionalLight.position.set(0, 5, 0);
// 设置光源投射阴影
// .castShadow : Boolean
// 此属性设置为 true 灯光将投射阴影。注意：这样做的代价比较高，需要通过调整让阴影看起来正确。 
// 查看 DirectionalLightShadow 了解详细信息。 默认值为 false。
directionalLight.castShadow = true;
scene.add(light);
scene.add(directionalLight);


// 4.渲染
// 初始化渲染器
// WebGLRenderer WebGL Render 用WebGL渲染出你精心制作的场景。
const renderer = new THREE.WebGLRenderer();
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// .domElement : DOMElement   一个canvas，渲染器在其上绘制输出。
// 渲染器的构造函数会自动创建(如果没有传入canvas参数);你需要做的仅仅是像下面这样将它加页面里去:
document.body.appendChild(renderer.domElement); //将绘制canvas添加到页面里
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;
renderer.physicallyCorrectLights = true; // 新版本不用设置 物理正确光源 decay也会生效。 老版本开阴影要设置


//创建轨道控制器
// OrbitControls( object : Camera, domElement : HTMLDOMElement )
// object: （必须）将要被控制的相机。该相机不允许是其他任何对象的子级，除非该对象是场景自身。
// domElement: 用于事件监听的HTML元素。
const controls = new OrbitControls(camera, renderer.domElement);
// 将其设置为true以启用阻尼（惯性），这将给控制器带来重量感。默认值为false。
// 请注意，如果该值被启用，你将必须在你的动画循环里调用.update()。
controls.enableDamping = true;

// 创建坐标轴辅助器
// AxesHelper  用于简单模拟3个坐标轴的对象.
// 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴.
// AxesHelper( size : Number )  size -- (可选的) 表示代表轴的线段长度. 默认为 1.
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

// 设置时钟
// Clock该对象用于跟踪时间。如果performance.now可用，则 Clock 对象通过该方法实现，否则回落到使用略欠精准的Date.now来实现。
// .oldTime : Float 存储时钟最后一次调用 start, .getElapsedTime() 或 .getDelta() 方法的时间。默认值是 0。
const clock = new THREE.Clock();


function render() {

    let deltaTime = clock.getDelta();

    // 开始更新计算物理世界的物体
    // step 推动物理世界向前发展。
    // 有两种模式。简单模式是固定时间步长，无需插值。在这种情况下，您仅使用第一个参数。
    // 第二种情况使用插值。您还可以提供自上次使用该函数以来的时间，以及要采取的最大固定时间步长。
    world.step(1 / 140, deltaTime);

    // 物理世界同步更新到three
    // box.position.copy(boxBody.position);
    for (const item of boxArr) {
        item.mesh.position.copy(item.body.position);
        item.mesh.quaternion.copy(item.body.quaternion)
    }
    controls.update();
    renderer.render(scene, camera);
    // requestAnimationFrame 是一个用于优化浏览器动画效果的 API。它可以让浏览器在下一次重绘前执行指定的回调函数，
    // 从而可以更加流畅地执行动画效果，避免了使用 setTimeout 或 setInterval 可能引起的性能问题。
    requestAnimationFrame(render);
}

render()

// 窗口变化时，更新渲染画面
window.addEventListener("resize", () => {

    // 更新摄像机视锥体长宽比
    camera.aspect = window.innerWidth / window.innerHeight;
    // 更新摄像机投影矩阵。在任何参数被改变以后必须被调用
    camera.updateProjectionMatrix();

    // 更新渲染器
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 设置渲染器像素比 通常用于避免HiDPI设备上绘图模糊
    renderer.setPixelRatio(window.devicePixelRatio);

})


